package org.codefilarete.stalactite.engine.configurer.map;

import java.util.Collections;
import java.util.Map;
import java.util.function.Function;

import org.apache.commons.lang3.builder.MultilineRecursiveToStringStyle;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.assertj.core.presentation.StandardRepresentation;
import org.codefilarete.stalactite.engine.configurer.map.KeyValueRecordIdMapping.RecordIdAssembler;
import org.codefilarete.stalactite.mapping.id.assembly.ComposedIdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.assembly.IdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.assembly.SingleIdentifierAssembler;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.stalactite.sql.result.MapBasedColumnedRow;
import org.codefilarete.tool.collection.Maps;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class KeyValueRecordIdMappingTest {
	
	@Test
	<T extends Table<T>, LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>>
	void assemble_mapEntryKeyIsASimpleProperty_recordIdContainsIdentifiers() {
		T joinTable = (T) new Table("joinTable");
		Column<T, Long> joinTableIdColumn = joinTable.addColumn("id", long.class);
		Column<T, String> entryKeyColumn = joinTable.addColumn("entryKey", String.class);
		LEFTTABLE leftTable = (LEFTTABLE) new Table("leftTable");
		Column<LEFTTABLE, Long> leftTableIdColumn = leftTable.addColumn("id", long.class);
		Function<ColumnedRow, String> entryKeyAssembler = columnedRow -> columnedRow.get(entryKeyColumn);
		IdentifierAssembler<Long, LEFTTABLE> sourceIdentifierAssembler = new SingleIdentifierAssembler<>(leftTableIdColumn);
		// Since we test assemble() method there's no need of columns mapping because they are used at insertion time
		Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping = Collections.emptyMap();
		RecordIdAssembler<String, Long, T> testInstance = new RecordIdAssembler<>(
				joinTable,
				entryKeyAssembler,
				s -> {
					throw new RuntimeException("This code should be called since its used in getColumnValues()");
				},
				sourceIdentifierAssembler,
				primaryKey2ForeignKeyMapping
		);
		
		MapBasedColumnedRow row = new MapBasedColumnedRow();
		row.add(leftTableIdColumn, 42L);
		row.add(entryKeyColumn, "toto");
		RecordId<String, Long> actual = testInstance.assemble(row);
		assertThat(actual)
				.withRepresentation(new StandardRepresentation() {
					@Override
					protected String fallbackToStringOf(Object object) {
						return ToStringBuilder.reflectionToString(object, new MultilineRecursiveToStringStyle());
					}
				})
				.isEqualTo(new RecordId<>(42L, "toto"));
	}
	
	@Test
	<T extends Table<T>, LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>>
	void assemble_mapEntryKeyIsEntityWithSingleColumn_recordIdContainsIdentifiers() {
		T joinTable = (T) new Table("joinTable");
		Column<T, Long> joinTableIdColumn = joinTable.addColumn("id", long.class);
		Column<T, String> entryKeyColumn = joinTable.addColumn("entryKey", String.class);
		LEFTTABLE leftTable = (LEFTTABLE) new Table("leftTable");
		Column<LEFTTABLE, Long> leftTableIdColumn = leftTable.addColumn("id", long.class);
		RIGHTTABLE rightTable = (RIGHTTABLE) new Table("rightTable");
		Column<RIGHTTABLE, String> rightTableIdColumn = rightTable.addColumn("entryKey", String.class);
		IdentifierAssembler<String, RIGHTTABLE> entryKeyIdentifierAssembler = new SingleIdentifierAssembler<>(rightTableIdColumn);
		IdentifierAssembler<Long, LEFTTABLE> sourceIdentifierAssembler = new SingleIdentifierAssembler<>(leftTableIdColumn);
		// Since we test assemble() method there's no need of columns mapping because they are used at insertion time
		Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping = Collections.emptyMap();
		Map<Column<RIGHTTABLE, ?>, Column<T, ?>> rightTable2EntryKeyMapping = Collections.emptyMap();
		RecordIdAssembler<String, Long, T> testInstance = new RecordIdAssembler<>(
				joinTable,
				entryKeyIdentifierAssembler,
				sourceIdentifierAssembler,
				primaryKey2ForeignKeyMapping,
				rightTable2EntryKeyMapping
		);
		
		MapBasedColumnedRow row = new MapBasedColumnedRow();
		row.add(leftTableIdColumn, 42L);
		row.add(rightTableIdColumn, "toto");
		RecordId<String, Long> actual = testInstance.assemble(row);
		assertThat(actual)
				.withRepresentation(new StandardRepresentation() {
					@Override
					protected String fallbackToStringOf(Object object) {
						return ToStringBuilder.reflectionToString(object, new MultilineRecursiveToStringStyle());
					}
				})
				.isEqualTo(new RecordId<>(42L, "toto"));
	}
	
	@Test
	<T extends Table<T>, LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>>
	void getColumnValues_mapEntryKeyIsASimpleProperty_givesRightValues() {
		T joinTable = (T) new Table("joinTable");
		Column<T, Long> joinTableIdColumn = joinTable.addColumn("id", long.class);
		Column<T, String> entryKeyColumn = joinTable.addColumn("entryKey", String.class);
		LEFTTABLE leftTable = (LEFTTABLE) new Table("leftTable");
		Column<LEFTTABLE, Long> leftTableIdColumn = leftTable.addColumn("id", long.class);
		Function<ColumnedRow, String> entryKeyAssembler = columnedRow -> columnedRow.get(entryKeyColumn);
		IdentifierAssembler<Long, LEFTTABLE> sourceIdentifierAssembler = new SingleIdentifierAssembler<>(leftTableIdColumn);
		Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping = (Map) Maps.forHashMap(Column.class, Column.class)
				.add(leftTableIdColumn, joinTableIdColumn);
		RecordIdAssembler<String, Long, T> testInstance = new RecordIdAssembler<>(
				joinTable,
				entryKeyAssembler,
				s -> (Map) Maps.forHashMap(Column.class, String.class)
						.add(entryKeyColumn, s),
				sourceIdentifierAssembler,
				primaryKey2ForeignKeyMapping
		);
		
		
		Map<Column<T, ?>, ?> actual = testInstance.getColumnValues(new RecordId<>(42L, "toto"));
		assertThat(actual).isEqualTo(Maps.forHashMap(Column.class, Object.class)
				.add(joinTableIdColumn, 42L)
				.add(entryKeyColumn, "toto"));
	}
	
	@Test
	<T extends Table<T>, LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>>
	void getColumnValues_mapEntryKeyIsEntityWithCompositeKey_givesRightValues() {
		T joinTable = (T) new Table("joinTable");
		Column<T, Long> joinTableIdColumn = joinTable.addColumn("id", long.class);
		Column<T, Long> entryKeyProp1Column = joinTable.addColumn("entryKey_prop1", Long.class);
		Column<T, String> entryKeyProp2Column = joinTable.addColumn("entryKey_prop2", String.class);
		LEFTTABLE leftTable = (LEFTTABLE) new Table("leftTable");
		Column<LEFTTABLE, Long> leftTableIdColumn = leftTable.addColumn("id", long.class);
		RIGHTTABLE rightTable = (RIGHTTABLE) new Table("rightTable");
		Column<RIGHTTABLE, Long> rightTableEntryKeyProp1Column = rightTable.addColumn("entryKey_prop1", Long.class);
		Column<RIGHTTABLE, String> rightTableEntryKeyProp2Column = rightTable.addColumn("entryKey_prop2", String.class);
		IdentifierAssembler<EntryKey, RIGHTTABLE> entryKeyIdentifierAssembler = new ComposedIdentifierAssembler<EntryKey, RIGHTTABLE>(rightTable) {
			@Override
			public EntryKey assemble(ColumnedRow columnValueProvider) {
				return new EntryKey(columnValueProvider.get(entryKeyProp1Column), columnValueProvider.get(entryKeyProp2Column));
			}
			
			@Override
			public Map<Column<RIGHTTABLE, ?>, Object> getColumnValues(EntryKey id) {
				return Maps.forHashMap((Class<Column<RIGHTTABLE, ?>>) (Class) Column.class, Object.class)
						.add(rightTableEntryKeyProp1Column, id.prop1)
						.add(rightTableEntryKeyProp2Column, id.prop2);
			}
		};
		IdentifierAssembler<Long, LEFTTABLE> sourceIdentifierAssembler = new SingleIdentifierAssembler<>(leftTableIdColumn);
		Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping = (Map) Maps.forHashMap(Column.class, Column.class)
				.add(leftTableIdColumn, joinTableIdColumn);
		Map<Column<RIGHTTABLE, ?>, Column<T, ?>> rightTable2EntryKeyMapping = (Map) Maps.forHashMap(Column.class, Column.class)
				.add(rightTableEntryKeyProp1Column, entryKeyProp1Column)
				.add(rightTableEntryKeyProp2Column, entryKeyProp2Column);
		RecordIdAssembler<EntryKey, Long, T> testInstance = new RecordIdAssembler<>(
				joinTable,
				entryKeyIdentifierAssembler,
				sourceIdentifierAssembler,
				primaryKey2ForeignKeyMapping,
				rightTable2EntryKeyMapping
		);
		
		Map<Column<T, ?>, ?> actual = testInstance.getColumnValues(new RecordId<>(42L, new EntryKey(17, "toto")));
		assertThat(actual).isEqualTo(Maps.forHashMap(Column.class, Object.class)
				.add(joinTableIdColumn, 42L)
				.add(entryKeyProp1Column, 17L)
				.add(entryKeyProp2Column, "toto"));
	}
	
	private static class EntryKey {
		
		private final long prop1;
		private final String prop2;
		
		public EntryKey(long prop1, String prop2) {
			this.prop1 = prop1;
			this.prop2 = prop2;
		}
	}
}
